/**
 * --------------------------------------------------------------------------
 * Bootstrap (v4.3.1): dropdown.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

import $ from 'jquery'
import Popper from 'popper.js'
import Util from './util'

/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */

const NAME = 'dropdown'
const VERSION = '4.3.1'
const DATA_KEY = 'bs.dropdown'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const JQUERY_NO_CONFLICT = $.fn[NAME]
const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key
const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key
const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key
const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key
const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key
const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)
const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)

const Event = {
    HIDE: `hide${EVENT_KEY}`,
    HIDDEN: `hidden${EVENT_KEY}`,
    SHOW: `show${EVENT_KEY}`,
    SHOWN: `shown${EVENT_KEY}`,
    CLICK: `click${EVENT_KEY}`,
    CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`,
    KEYDOWN_DATA_API: `keydown${EVENT_KEY}${DATA_API_KEY}`,
    KEYUP_DATA_API: `keyup${EVENT_KEY}${DATA_API_KEY}`
}

const ClassName = {
    DISABLED: 'disabled',
    SHOW: 'show',
    DROPUP: 'dropup',
    DROPRIGHT: 'dropright',
    DROPLEFT: 'dropleft',
    MENURIGHT: 'dropdown-menu-right',
    MENULEFT: 'dropdown-menu-left',
    POSITION_STATIC: 'position-static'
}

const Selector = {
    DATA_TOGGLE: '[data-toggle="dropdown"]',
    FORM_CHILD: '.dropdown form',
    MENU: '.dropdown-menu',
    NAVBAR_NAV: '.navbar-nav',
    VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
}

const AttachmentMap = {
    TOP: 'top-start',
    TOPEND: 'top-end',
    BOTTOM: 'bottom-start',
    BOTTOMEND: 'bottom-end',
    RIGHT: 'right-start',
    RIGHTEND: 'right-end',
    LEFT: 'left-start',
    LEFTEND: 'left-end'
}

const Default = {
    offset: 0,
    flip: true,
    boundary: 'scrollParent',
    reference: 'toggle',
    display: 'dynamic'
}

const DefaultType = {
    offset: '(number|string|function)',
    flip: 'boolean',
    boundary: '(string|element)',
    reference: '(string|element)',
    display: 'string'
}

/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */

class Dropdown {
    constructor(element, config) {
        this._element = element
        this._popper = null
        this._config = this._getConfig(config)
        this._menu = this._getMenuElement()
        this._inNavbar = this._detectNavbar()

        this._addEventListeners()
    }

    // Getters

    static get VERSION() {
        return VERSION
    }

    static get Default() {
        return Default
    }

    static get DefaultType() {
        return DefaultType
    }

    // Public

    toggle() {
        if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {
            return
        }

        const parent = Dropdown._getParentFromElement(this._element)
        const isActive = $(this._menu).hasClass(ClassName.SHOW)

        Dropdown._clearMenus()

        if (isActive) {
            return
        }

        const relatedTarget = {
            relatedTarget: this._element
        }
        const showEvent = $.Event(Event.SHOW, relatedTarget)

        $(parent).trigger(showEvent)

        if (showEvent.isDefaultPrevented()) {
            return
        }

        // Disable totally Popper.js for Dropdown in Navbar
        if (!this._inNavbar) {
            /**
             * Check for Popper dependency
             * Popper - https://popper.js.org
             */
            if (typeof Popper === 'undefined') {
                throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)')
            }

            let referenceElement = this._element

            if (this._config.reference === 'parent') {
                referenceElement = parent
            } else if (Util.isElement(this._config.reference)) {
                referenceElement = this._config.reference

                // Check if it's jQuery element
                if (typeof this._config.reference.jquery !== 'undefined') {
                    referenceElement = this._config.reference[0]
                }
            }

            // If boundary is not `scrollParent`, then set position to `static`
            // to allow the menu to "escape" the scroll parent's boundaries
            // https://github.com/twbs/bootstrap/issues/24251
            if (this._config.boundary !== 'scrollParent') {
                $(parent).addClass(ClassName.POSITION_STATIC)
            }
            this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())
        }

        // If this is a touch-enabled device we add extra
        // empty mouseover listeners to the body's immediate children;
        // only needed because of broken event delegation on iOS
        // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
        if ('ontouchstart' in document.documentElement &&
            $(parent).closest(Selector.NAVBAR_NAV).length === 0) {
            $(document.body).children().on('mouseover', null, $.noop)
        }

        this._element.focus()
        this._element.setAttribute('aria-expanded', true)

        $(this._menu).toggleClass(ClassName.SHOW)
        $(parent)
            .toggleClass(ClassName.SHOW)
            .trigger($.Event(Event.SHOWN, relatedTarget))
    }

    show() {
        if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) {
            return
        }

        const relatedTarget = {
            relatedTarget: this._element
        }
        const showEvent = $.Event(Event.SHOW, relatedTarget)
        const parent = Dropdown._getParentFromElement(this._element)

        $(parent).trigger(showEvent)

        if (showEvent.isDefaultPrevented()) {
            return
        }

        $(this._menu).toggleClass(ClassName.SHOW)
        $(parent)
            .toggleClass(ClassName.SHOW)
            .trigger($.Event(Event.SHOWN, relatedTarget))
    }

    hide() {
        if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) {
            return
        }

        const relatedTarget = {
            relatedTarget: this._element
        }
        const hideEvent = $.Event(Event.HIDE, relatedTarget)
        const parent = Dropdown._getParentFromElement(this._element)

        $(parent).trigger(hideEvent)

        if (hideEvent.isDefaultPrevented()) {
            return
        }

        $(this._menu).toggleClass(ClassName.SHOW)
        $(parent)
            .toggleClass(ClassName.SHOW)
            .trigger($.Event(Event.HIDDEN, relatedTarget))
    }

    dispose() {
        $.removeData(this._element, DATA_KEY)
        $(this._element).off(EVENT_KEY)
        this._element = null
        this._menu = null
        if (this._popper !== null) {
            this._popper.destroy()
            this._popper = null
        }
    }

    update() {
        this._inNavbar = this._detectNavbar()
        if (this._popper !== null) {
            this._popper.scheduleUpdate()
        }
    }

    // Private

    _addEventListeners() {
        $(this._element).on(Event.CLICK, (event) => {
            event.preventDefault()
            event.stopPropagation()
            this.toggle()
        })
    }

    _getConfig(config) {
        config = {
            ...this.constructor.Default,
            ...$(this._element).data(),
            ...config
        }

        Util.typeCheckConfig(
            NAME,
            config,
            this.constructor.DefaultType
        )

        return config
    }

    _getMenuElement() {
        if (!this._menu) {
            const parent = Dropdown._getParentFromElement(this._element)

            if (parent) {
                this._menu = parent.querySelector(Selector.MENU)
            }
        }
        return this._menu
    }

    _getPlacement() {
        const $parentDropdown = $(this._element.parentNode)
        let placement = AttachmentMap.BOTTOM

        // Handle dropup
        if ($parentDropdown.hasClass(ClassName.DROPUP)) {
            placement = AttachmentMap.TOP
            if ($(this._menu).hasClass(ClassName.MENURIGHT)) {
                placement = AttachmentMap.TOPEND
            }
        } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {
            placement = AttachmentMap.RIGHT
        } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {
            placement = AttachmentMap.LEFT
        } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {
            placement = AttachmentMap.BOTTOMEND
        }
        return placement
    }

    _detectNavbar() {
        return $(this._element).closest('.navbar').length > 0
    }

    _getOffset() {
        const offset = {}

        if (typeof this._config.offset === 'function') {
            offset.fn = (data) => {
                data.offsets = {
                    ...data.offsets,
                    ...this._config.offset(data.offsets, this._element) || {}
                }

                return data
            }
        } else {
            offset.offset = this._config.offset
        }

        return offset
    }

    _getPopperConfig() {
        const popperConfig = {
            placement: this._getPlacement(),
            modifiers: {
                offset: this._getOffset(),
                flip: {
                    enabled: this._config.flip
                },
                preventOverflow: {
                    boundariesElement: this._config.boundary
                }
            }
        }

        // Disable Popper.js if we have a static display
        if (this._config.display === 'static') {
            popperConfig.modifiers.applyStyle = {
                enabled: false
            }
        }

        return popperConfig
    }

    // Static

    static _jQueryInterface(config) {
        return this.each(function() {
            let data = $(this).data(DATA_KEY)
            const _config = typeof config === 'object' ? config : null

            if (!data) {
                data = new Dropdown(this, _config)
                $(this).data(DATA_KEY, data)
            }

            if (typeof config === 'string') {
                if (typeof data[config] === 'undefined') {
                    throw new TypeError(`No method named "${config}"`)
                }
                data[config]()
            }
        })
    }

    static _clearMenus(event) {
        if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||
                event.type === 'keyup' && event.which !== TAB_KEYCODE)) {
            return
        }

        const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))

        for (let i = 0, len = toggles.length; i < len; i++) {
            const parent = Dropdown._getParentFromElement(toggles[i])
            const context = $(toggles[i]).data(DATA_KEY)
            const relatedTarget = {
                relatedTarget: toggles[i]
            }

            if (event && event.type === 'click') {
                relatedTarget.clickEvent = event
            }

            if (!context) {
                continue
            }

            const dropdownMenu = context._menu
            if (!$(parent).hasClass(ClassName.SHOW)) {
                continue
            }

            if (event && (event.type === 'click' &&
                    /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&
                $.contains(parent, event.target)) {
                continue
            }

            const hideEvent = $.Event(Event.HIDE, relatedTarget)
            $(parent).trigger(hideEvent)
            if (hideEvent.isDefaultPrevented()) {
                continue
            }

            // If this is a touch-enabled device we remove the extra
            // empty mouseover listeners we added for iOS support
            if ('ontouchstart' in document.documentElement) {
                $(document.body).children().off('mouseover', null, $.noop)
            }

            toggles[i].setAttribute('aria-expanded', 'false')

            $(dropdownMenu).removeClass(ClassName.SHOW)
            $(parent)
                .removeClass(ClassName.SHOW)
                .trigger($.Event(Event.HIDDEN, relatedTarget))
        }
    }

    static _getParentFromElement(element) {
        let parent
        const selector = Util.getSelectorFromElement(element)

        if (selector) {
            parent = document.querySelector(selector)
        }

        return parent || element.parentNode
    }

    // eslint-disable-next-line complexity
    static _dataApiKeydownHandler(event) {
        // If not input/textarea:
        //  - And not a key in REGEXP_KEYDOWN => not a dropdown command
        // If input/textarea:
        //  - If space key => not a dropdown command
        //  - If key is other than escape
        //    - If key is not up or down => not a dropdown command
        //    - If trigger inside the menu => not a dropdown command
        if (/input|textarea/i.test(event.target.tagName) ?
            event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&
            (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||
                $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {
            return
        }

        event.preventDefault()
        event.stopPropagation()

        if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {
            return
        }

        const parent = Dropdown._getParentFromElement(this)
        const isActive = $(parent).hasClass(ClassName.SHOW)

        if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {
            if (event.which === ESCAPE_KEYCODE) {
                const toggle = parent.querySelector(Selector.DATA_TOGGLE)
                $(toggle).trigger('focus')
            }

            $(this).trigger('click')
            return
        }

        const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))

        if (items.length === 0) {
            return
        }

        let index = items.indexOf(event.target)

        if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up
            index--
        }

        if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down
            index++
        }

        if (index < 0) {
            index = 0
        }

        items[index].focus()
    }
}

/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */

$(document)
    .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)
    .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)
    .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)
    .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function(event) {
        event.preventDefault()
        event.stopPropagation()
        Dropdown._jQueryInterface.call($(this), 'toggle')
    })
    .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {
        e.stopPropagation()
    })

/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
 */

$.fn[NAME] = Dropdown._jQueryInterface
$.fn[NAME].Constructor = Dropdown
$.fn[NAME].noConflict = () => {
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Dropdown._jQueryInterface
}


export default Dropdown